package com.zzyk.main.configuration.shiro.realm;


import cn.hutool.core.date.DateTime;
import com.zzyk.main.configuration.constant.CommonConstant;
import com.zzyk.main.configuration.jwt.JwtToken;
import com.zzyk.main.configuration.jwt.JwtUtil;
import com.zzyk.main.model.pojo.SysPermission;
import com.zzyk.main.model.pojo.SysRole;
import com.zzyk.main.model.pojo.SysUser;
import com.zzyk.main.service.ISysUserService;
import com.zzyk.main.utils.IPUtils;
import com.zzyk.main.utils.RedisUtils;
import com.zzyk.main.utils.SpringContextUtils;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * Shiro权限管理的认证细节在这里实现
 */
@Log4j2
public class JwtRealm extends AuthorizingRealm {

    @Autowired
    private ApplicationContext ctx;
    private ISysUserService userService;
    private RedisUtils redisUtils;

    /**
     * 大坑！，必须重写此方法，不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 功能： 获取用户权限信息，包括角色以及权限。只有当触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
     *
     * @param principalCollection token
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 一定要这么写 绝对不能用自动注入 否则多数据源将失效(注入顺序的原因)
        if (this.userService == null) {
            this.userService = ctx.getBean(ISysUserService.class);
            this.redisUtils = ctx.getBean(RedisUtils.class);
        }
        SysUser sysUser = null;
        String username = null;
        if (principalCollection != null) {
            sysUser = (SysUser) principalCollection.getPrimaryPrincipal();
            username = sysUser.getUsername();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> strings = (Set<String>) redisUtils.get(username + "roleSet");
        Set<String> permissionSets = (Set<String>) redisUtils.get(username + "permissionSet");
        // 然后把他的角色和权限查出来 扔进
        Set<String> roleSet = new HashSet<>();
        Set<String> permissionSet = new HashSet<>();
     /*   if (ObjectUtil.isNull(strings) || strings.isEmpty() || ObjectUtil.isNull(permissionSets) || permissionSets.isEmpty()) {
            // 首先要把用户查出来
            SysUser user = userService.getUserWithRoleAndPermission(username);
            for (SysRole role : user.getRoleSet()) {
                roleSet.add(role.getRoleName());
                for (SysPermission permission : role.getPermissionSet()) {
                    permissionSet.add(permission.getPermissionName());
                }
            }
            redisUtils.set(username + "roleSet", roleSet, JwtUtil.EXPIRE_TIME / 1000);
            redisUtils.set(username + "permissionSet", permissionSet, JwtUtil.EXPIRE_TIME / 1000);
            info.setRoles(roleSet);
            info.setStringPermissions(permissionSet);
        } else {
            info.setRoles(strings);
            info.setStringPermissions(permissionSets);
        }*/
        // 首先要把用户查出来
        SysUser user = userService.getUserWithRoleAndPermission(username);
        for (SysRole role : user.getRoleSet()) {
            roleSet.add(role.getRoleName());
            for (SysPermission permission : role.getPermissionSet()) {
                permissionSet.add(permission.getPermissionName());
            }
        }
        info.setRoles(roleSet);
        info.setStringPermissions(permissionSet);
        return info;
    }

    /**
     * 功能： 用来进行身份认证，也就是说验证用户输入的账号和密码是否正确，获取身份验证信息，错误抛出异常
     *
     * @param authenticationToken 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (this.userService == null) {
            this.userService = ctx.getBean(ISysUserService.class);
            this.redisUtils = ctx.getBean(RedisUtils.class);
        }
        String token = (String) authenticationToken.getCredentials();
        if (StringUtils.isBlank(token)) {
            log.info("————————身份认证失败——————————IP地址:  " + IPUtils.getIpAddr(SpringContextUtils.getHttpServletRequest()));
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        SysUser loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     *
     * @param token
     */
    public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username，用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }
        // 查询用户信息
        SysUser loginUser = new SysUser();
        SysUser sysUser = userService.login(username);
        if (sysUser == null) {
            throw new AuthenticationException("用户不存在!");
        }

        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, sysUser.getPassword())) {
            throw new AuthenticationException("Token失效请重新登录!");
        }

        // 判断用户状态
        if (sysUser.getStatus() == 0) {
            throw new AuthenticationException("账号审核中，审核通过会有短信告知，急用联系渠道人员。");
        }
        Date expire = sysUser.getExpire();
        if (expire != null) {
            DateTime ex = new DateTime(expire);
            if (ex.isBefore(new Date())) {
                SysUser userInfo = new SysUser();
                userInfo.setId(sysUser.getId());
                userInfo.setUpdateTime(new Date());
                userInfo.setStatus(0);
                userService.updateUser(userInfo);
                throw new AuthenticationException("账号已过期");
            }
        }
        BeanUtils.copyProperties(sysUser, loginUser);
        return loginUser;
    }


    /**
     * JWTToken刷新生命周期 （解决用户一直在线操作，提供Token失效问题）
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWTToken值还在生命周期内，则会通过重新PUT的方式k、v都为Token值，缓存中的token值生命周期时间重新计算(这时候k、v值一样)
     * 4、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 5、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 6、每次当返回为true情况下，都会给Response的Header中设置Authorization，该Authorization映射的v为cache对应的v值。
     * 7、注：当前端接收到Response的Header中的Authorization值会存储起来，作为以后请求token使用
     * 参考方案：https://blog.csdn.net/qq394829044/article/details/82763936
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        String cacheToken = String.valueOf(redisUtils.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (StringUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.sign(userName, passWord);
                redisUtils.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                // 设置超时时间
                redisUtils.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
            } else {
                redisUtils.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
                // 设置超时时间
                redisUtils.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
            }
            return true;
        }
        return false;
    }
}
